package parser;

import java.util.ArrayList;
import scanner.LexicalException;
import scanner.Scanner;
import scanner.Token;
import util.NodeElsif;
import util.AST.*;

/**
 * Parser class
 * @version 2010-august-29
 * @discipline Projeto de Compiladores
 * @author Gustavo H P Carvalho
 * @email gustavohpcarvalho@ecomp.poli.br
 */
public class Parser {

	// The current token
	private Token currentToken = null;
	// The scanner
	private Scanner scanner = null;
	
	/**
	 * Parser constructor
	 * @throws LexicalException 
	 */
	public Parser(){
		// Initializes the scanner object
		this.scanner = new Scanner();

		try {
			this.currentToken = scanner.getNextToken();
		} catch (LexicalException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	/**
	 * Verifies if the current token kind is the expected one
	 * @param kind
	 * @throws SyntacticException
	 * @throws LexicalException 
	 */ //TODO
	private void accept(int kind) throws SyntacticException, LexicalException {
		// If the current token kind is equal to the expected
		if(this.currentToken.getKind() == kind){
			// Gets next token
			this.currentToken = scanner.getNextToken();
		}else{// If not
			// Raises an exception
			throw new SyntacticException("Token nao esperado", this.currentToken);
		}	
	}
	
	/**
	 * Gets next token
	 */ //TODO
	private void acceptIt() throws LexicalException{
		this.currentToken = scanner.getNextToken();
	}

	/**
	 * Verifies if the source program is syntactically correct
	 * @throws SyntacticException
	 */ //TODO
	public AST parse() throws SyntacticException {
		AST astReturn = null;
		try {
			astReturn = programa();
			accept(TokenType.EOT);
		} catch (LexicalException e) {
			e.printStackTrace();
		}
		return astReturn;
	}

	public ASTexpPar expPar() throws SyntacticException, LexicalException{
		//caso o token atual seja um parenteses esquerdo entao e uma expresao com parentesse
		if(this.currentToken.getKind() == TokenType.PARENTESESESQUERDO){ 
			acceptIt();
			ASTexpParAdicao expressaoDeAdicao = new ASTexpParAdicao(expAriAdi());
			accept(TokenType.PARENTESESDIREITO);
			return expressaoDeAdicao;
		//se ele for um $ então o fator e o retorno de uma funcao
		}else if(this.currentToken.getKind() == TokenType.$){
			return new ASTexpParFuncao(chamFunc());
		//neste caso o fator e um numero
		}else if(this.currentToken.getKind() == TokenType.NUMERO){
			LiteralNumerica numero = new LiteralNumerica(this.currentToken.getSpelling()); 
			acceptIt();
			return new ASTexpParNumero(numero);
		//caso o fator seja um 
		}else if(this.currentToken.getKind() == TokenType.ID){
			Identificador id = new Identificador(this.currentToken.getSpelling());
			acceptIt();
			return new ASTexpParIdentificador(id);
		//neste caso o fator e um numero com sinal na frente
		}else if(this.currentToken.getKind() == TokenType.OPARIADI){
			OpAriAdi operador = new OpAriAdi(this.currentToken.getSpelling());
			acceptIt();
			return new ASTexpParExpressaoComSinal(operador, expPar());
		}else{
			throw new SyntacticException("Esperava-se um id, numero, chamada de funcao ou uma (", this.currentToken);
		}
	}
	
	public ASTexpAriMul expAriMul() throws SyntacticException, LexicalException{
		
		ArrayList<ASTexpPar> fatores = new ArrayList<ASTexpPar>();
		ArrayList<OpAriMul> operadores = new ArrayList<OpAriMul>();
		
		fatores.add(expPar());
		while(this.currentToken.getKind() == TokenType.OPARIMUL){
			operadores.add(new OpAriMul(this.currentToken.getSpelling()));
			acceptIt();
			fatores.add(expPar());
		}
		return new ASTexpAriMul(fatores,operadores);
	}
	
	public ASTexpAriAdi expAriAdi() throws SyntacticException, LexicalException{
		
		ArrayList<ASTexpAriMul> fatores = new ArrayList<ASTexpAriMul>();
		ArrayList<OpAriAdi> operadores = new ArrayList<OpAriAdi>();
		fatores.add(expAriMul());
		while(this.currentToken.getKind() == TokenType.OPARIADI){
			operadores.add(new OpAriAdi(this.currentToken.getSpelling()));
			acceptIt();
			fatores.add(expAriMul());
		}
		return new ASTexpAriAdi(fatores, operadores);
	}
	
	public ASTexpRel expRel() throws SyntacticException, LexicalException{
		
		ASTexpAriAdi[] fatores = new ASTexpAriAdi[2];
		OpRelacional operador;
		fatores[0] = expAriAdi();
		operador = new OpRelacional(this.currentToken.getSpelling());
		accept(TokenType.OPREL);
		fatores[1] = expAriAdi();
		
		return new ASTexpRel(fatores,operador);
	}

	public ASTexpressao expressao() throws SyntacticException, LexicalException{
		
		ASTexpAriAdi[] fatores = new ASTexpAriAdi[2];
		OpRelacional operador = new OpRelacional(null);
		fatores[0] = expAriAdi();
		if(this.currentToken.getKind() == TokenType.OPREL){
			operador.spelling = this.currentToken.getSpelling();
			acceptIt();
			fatores[1] = expAriAdi();
		}
		return new ASTexpressao(fatores, operador);
	}
	
	public ASTchamFunc chamFunc() throws SyntacticException, LexicalException{
		//aceita o $ testado anteriormente
		acceptIt();
		Identificador id = new Identificador(this.currentToken.getSpelling());
		ArrayList<ASTexpressao> argumentos = new ArrayList<ASTexpressao>();
		//aceita o nome da funcao
		accept(TokenType.ID);
		accept(TokenType.PARENTESESESQUERDO);
		//verifica se exite argumentos
		if(this.currentToken.getKind() != TokenType.PARENTESESDIREITO){
			//adicioana o primeiro argumento.
			argumentos.add(expressao());
		}
		//verifica se ha mais argumentos
		while(this.currentToken.getKind() == TokenType.VIRGULA){
			//aceitar a virgula
			acceptIt();
			//adiciona os outros argumentos;
			argumentos.add(expressao());
		}
		accept(TokenType.PARENTESESDIREITO);
		
		return new ASTchamFunc(id,argumentos);
	}

	public ASTdeclaDeVar declaDeVar() throws LexicalException, SyntacticException{
		
		ArrayList<Identificador> ids = new ArrayList<Identificador>();
		TipoDeVariavel tipo;
		ASTexpressao valorInicial = null;
		
		ids.add(new Identificador(this.currentToken.getSpelling()));
		// Aceita o id que ja foi testado antes de entrar
		acceptIt();
		
		//caso seja definido mais de uma variavel, elas deverao se separadas por virgula
		while(this.currentToken.getKind() == TokenType.VIRGULA){
			acceptIt();
			ids.add(new Identificador(this.currentToken.getSpelling()));
			//aceita a proxima variavel
			accept(TokenType.ID);
		}
		//dois pontos para definicao do tipo de variavel(is)
		accept(TokenType.DOISPONTOS);
		if(this.currentToken.getKind() == TokenType.INTEGER 
			|| this.currentToken.getKind() == TokenType.BOOLEAN){
			
			tipo = new TipoDeVariavel(this.currentToken.getSpelling());
			acceptIt();
		}else{
			throw new SyntacticException("Esperava  um tipo de variável", this.currentToken);
		}
		
		//case elas seja inicializadas com algum valor
		if(this.currentToken.getKind() == TokenType.OPATRIBUICAO){
			acceptIt();
			valorInicial = expressao();
		}
		accept(TokenType.PONTOEVIRGULA);
		return new ASTdeclaDeVar(ids, tipo, valorInicial);
	}
	
	public ASTcomandAtribuicao atribuicao() throws SyntacticException, LexicalException{
		
		Identificador id = new Identificador(this.currentToken.getSpelling());
		//aceita o id testado anteriomente
		acceptIt();
		accept(TokenType.OPATRIBUICAO);
		
		return new ASTcomandAtribuicao(id,expressao());
	}
	
	public ASTcomando comando() throws SyntacticException, LexicalException{
		
		ASTcomando comando = null;
		
		//caso comando de atribuicao
		if(this.currentToken.getKind() == TokenType.ID){
			comando = atribuicao();
		//caso comando de retorno
		}else if(this.currentToken.getKind() == TokenType.RETURN){
			acceptIt();
			Identificador id = new Identificador(this.currentToken.getSpelling());
			accept(TokenType.ID);
			comando = new ASTcomandRetorno(id);
		//caso comando break
		}else if(this.currentToken.getKind() == TokenType.BREAK){
			ASTcomandBreak comandoBreak = new ASTcomandBreak(this.currentToken.getSpelling());
			acceptIt();
			comando = comandoBreak;
		//caso comando continue
		}else if(this.currentToken.getKind() == TokenType.CONTINUE){
			ASTcomandContinue comandoContinue = new ASTcomandContinue(this.currentToken.getSpelling());
			acceptIt();
			comando = comandoContinue;
		//caso comando de imprimir na tela
		}else if(this.currentToken.getKind() == TokenType.PUT_LINE){
			acceptIt();
			accept(TokenType.PARENTESESESQUERDO);
			ASTcomandPrint impressora = new ASTcomandPrint(expressao());
			accept(TokenType.PARENTESESDIREITO);
			comando = impressora;
		//caso chamada de funcao
		}else if(this.currentToken.getKind() == TokenType.$){
			comando = chamFunc();
		//caso comando de repeticao
		}else if(this.currentToken.getKind() == TokenType.WHILE){
			comando = comandLaco();
		//caso comando de desvio
		}else if(this.currentToken.getKind() == TokenType.IF){
			comando = comandCond();
		}
		accept(TokenType.PONTOEVIRGULA);
		return comando;
	}

	public ASTcomandCond comandCond()throws SyntacticException, LexicalException{

		ASTexpRel condicaoIf;
		ArrayList<ASTcomando> comandosIf = new ArrayList<ASTcomando>();
		ArrayList<NodeElsif> elsifs = new ArrayList<NodeElsif>();
		NodeElsif tempElsif = null;
		ArrayList<ASTcomando> comandosElse = new ArrayList<ASTcomando>();

		//aceita o IF testado anteriormente
		acceptIt();
		//guarda a expressao para entrar no IF
		condicaoIf = expRel();
		accept(TokenType.THEN);
		while(this.currentToken.getKind() != TokenType.ELSE
				&& this.currentToken.getKind() != TokenType.ENDIF
				&& this.currentToken.getKind() != TokenType.ELSIF){
			
			//comandos do IF
			comandosIf.add(comando());
		}
		//caso exista ELSIF
		if(this.currentToken.getKind() == TokenType.ELSIF){
			while(this.currentToken.getKind() == TokenType.ELSIF){
				acceptIt();
				
				//cria um novo no
				tempElsif = new NodeElsif();
				//adiciona a expressao de um elsif
				tempElsif.condicao = expRel();
				accept(TokenType.THEN);
				while(this.currentToken.getKind() != TokenType.ELSE
						&& this.currentToken.getKind() != TokenType.ENDIF
						&& this.currentToken.getKind() != TokenType.ELSIF){
					
					//Adiciona um comando do ELSIF atual 
					tempElsif.comando.add(comando());
				}
				//adiciona um elsif ao array dos nos que tem uma expressao e uma lista de comnados
				elsifs.add(tempElsif);
			}
		}
		//caso exista ELSE
		if(this.currentToken.getKind() == TokenType.ELSE){
			acceptIt();
			while(this.currentToken.getKind() != TokenType.ENDIF){
				//comandos do ELSE
				comandosElse.add(comando());
			}
		}
		accept(TokenType.ENDIF);
		
		return new ASTcomandCond(condicaoIf, comandosIf, elsifs, comandosElse);
	}
	
	public ASTcomandLaco comandLaco() throws SyntacticException, LexicalException{
		ArrayList<ASTcomando> comandos = new ArrayList<ASTcomando>();
		ASTexpRel condicao;
		//aceita o while testado anteriormente
		acceptIt();
		condicao = expRel();
		accept(TokenType.LOOP);
		//"varre" todos os comandos
		while(this.currentToken.getKind() != TokenType.ENDLOOP){
			
			comandos.add(comando());
		}
		//aceita o endloop
		acceptIt();
		return new ASTcomandLaco(condicao, comandos);
	}
	
	public ASTparametro parametro() throws SyntacticException, LexicalException{
		
		ArrayList<Identificador> ids = new ArrayList<Identificador>();
		ArrayList<TipoDeVariavel> tipos = new ArrayList<TipoDeVariavel>();
		
		ids.add(new Identificador(this.currentToken.getSpelling()));
		//aceita o id testado anteriomente
		acceptIt();
		//guarda o tipo do parametro
		tipos.add(parametroaux());
		//caso tenha mais parametros para declaarar
		while(this.currentToken.getKind() == TokenType.VIRGULA){
			//aceita a virgula e começa tudo denovo
			acceptIt();
			ids.add(new Identificador(this.currentToken.getSpelling()));
			accept(TokenType.ID);
			tipos.add(parametroaux());
		}
		return new ASTparametro(ids,tipos);
		
	}
	
	//remove código semelhante da funcao parametro
	private TipoDeVariavel parametroaux() throws SyntacticException, LexicalException{
		
		TipoDeVariavel tipo;
		accept(TokenType.DOISPONTOS);
		//escolhe o tipo de parametro int ou bool
		if(this.currentToken.getKind() == TokenType.BOOLEAN
			|| this.currentToken.getKind() == TokenType.INTEGER){
			
			tipo = new TipoDeVariavel(this.currentToken.getSpelling());
			acceptIt();
			return tipo;
		}else{
			throw new SyntacticException("Esperava um tipo de variavel", this.currentToken);
		}
		
	}
	
	public ASTprocedimento procedure() throws SyntacticException, LexicalException{
		
		Identificador[] nomeDoProcedimento = new Identificador[2];
		ASTparametro parametro = null;
		ArrayList<ASTdeclaDeVar> declaDeVar = new ArrayList<ASTdeclaDeVar>();
		ArrayList<ASTcomando> comandos = new ArrayList<ASTcomando>();
		
		//acita o token procedure 
		acceptIt();
		//guarda o primeiro id
		nomeDoProcedimento[0] = new Identificador(this.currentToken.getSpelling());
		accept(TokenType.ID);
		//declaracao de parametro
		if(this.currentToken.getKind() == TokenType.PARENTESESESQUERDO){
			acceptIt();
			//guarda os parametros
			parametro = parametro();
			accept(TokenType.PARENTESESDIREITO);
		}
		accept(TokenType.IS);
		//caso tenha variaveis locais
		while(this.currentToken.getKind() != TokenType.BEGIN){
			declaDeVar.add(declaDeVar());
		}
		//aceita o BEGIN
		acceptIt();
		while(this.currentToken.getKind() != TokenType.END){
			//comandos do procedimento
			comandos.add(comando());
		}
		//aceita o token END
		acceptIt();
		//guarda o segundo id da funcao
		nomeDoProcedimento[1] = new Identificador(this.currentToken.getSpelling());
		accept(TokenType.ID);
		accept(TokenType.PONTOEVIRGULA);
		return new ASTprocedimento(nomeDoProcedimento, parametro, declaDeVar, comandos);
	}

	public ASTfuncao funcao() throws SyntacticException, LexicalException{
		
		Identificador[] nomeDaFuncao = new Identificador[2];
		ASTparametro parametro = null;
		TipoDeVariavel tipoDeRetorno;
		ArrayList<ASTdeclaDeVar> declaDeVar = new ArrayList<ASTdeclaDeVar>();
		ArrayList<ASTcomando> comandos = new ArrayList<ASTcomando>();
		
		//aceita o token function testado antes
		acceptIt();
		//guarda o nome da funcao
		nomeDaFuncao[0] = new Identificador(this.currentToken.getSpelling());
		accept(TokenType.ID);
		// nas funcaoes os parenteses sao obrigatorios
		accept(TokenType.PARENTESESESQUERDO);
		if(this.currentToken.getKind() == TokenType.ID){
			parametro = parametro();
		}
		accept(TokenType.PARENTESESDIREITO);
		
		//retorno e obrigatorio
		accept(TokenType.RETURN);
		//qual o tipo de retorno
		if(this.currentToken.getKind() == TokenType.INTEGER 
				|| this.currentToken.getKind() == TokenType.BOOLEAN){
			tipoDeRetorno = new TipoDeVariavel(this.currentToken.getSpelling());
			acceptIt();
		}else{
			throw new SyntacticException("Esperava um tipo de variavel", this.currentToken);
		}
		accept(TokenType.IS);
		//variaveis locais
		while(this.currentToken.getKind() != TokenType.BEGIN){
			declaDeVar.add(declaDeVar());
		}
		//aceita o BEGIN
		acceptIt();
		while(this.currentToken.getKind() != TokenType.END){
			//comandos da funcao
			comandos.add(comando());
		}
		//aceita o END
		acceptIt();
		//guarda o nome da funcao pela segunda vez
		nomeDaFuncao[1] = new Identificador(this.currentToken.getSpelling());
		accept(TokenType.ID);
		accept(TokenType.PONTOEVIRGULA);
		return new ASTfuncao(nomeDaFuncao, parametro, tipoDeRetorno, declaDeVar, comandos);
	}

	public ASTprograma programa() throws SyntacticException, LexicalException{
		
		Identificador[] nomeDoPrograma = new Identificador[2];
		ArrayList<ASTdeclaDeVar> declaDeVar = new ArrayList<ASTdeclaDeVar>();
		ArrayList<ASTprocedimento> procedimentos = new ArrayList<ASTprocedimento>();
		ArrayList<ASTfuncao> funcoes = new ArrayList<ASTfuncao>();
		ArrayList<ASTcomando> comandos = new ArrayList<ASTcomando>();
		
		accept(TokenType.PROCEDURE);
		//guarda nome do programa
		nomeDoPrograma[0] = new Identificador(this.currentToken.getSpelling()); 
		accept(TokenType.ID);
		accept(TokenType.IS);
		while(this.currentToken.getKind() != TokenType.BEGIN){
			if(this.currentToken.getKind() == TokenType.ID){
				//guarda as declaracoes de variaveis
				declaDeVar.add(declaDeVar());
			}else if(this.currentToken.getKind() == TokenType.PROCEDURE){
				//guarda os procedimentos
				procedimentos.add(procedure());
			}else{
				//guarda as funcoes
				funcoes.add(funcao());
			}
		}
		//aceita o token BEGIN
		acceptIt();
		while(this.currentToken.getKind() != TokenType.END){
			//comando do programa
			comandos.add(comando());
		}
		//aceita o END
		acceptIt();
		//guarda o nome do programa
		nomeDoPrograma[1] = new Identificador(this.currentToken.getSpelling());
		accept(TokenType.ID);
		accept(TokenType.PONTOEVIRGULA);
		return new ASTprograma(nomeDoPrograma, declaDeVar, procedimentos, funcoes, comandos);
	}

}
